/* This source file is part of the Delectable Engine.
 * For the latest info, please visit http://delectable.googlecode.com/
 *
 * Copyright (c) 2009-2010 Ryan Chew
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *    http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @file Win32PlatformWindow.cpp
 * @brief Windows specific window implementation.
 */

// Delectable Headers //
#include "Win32PlatformWindow.h"
#include "dbl/Input/KeyCodes.h"
#include "dbl/Core/GameWindow.h"

// Chewable Headers //
#include <cbl/Core/Game.h>
#include <cbl/Debug/Logging.h>

using namespace dbl;

Key::Code ConvertVirtualCode( WPARAM virtualKey, LPARAM flags );

cbl::Uint32					Win32PlatformWindow::sWindowCount	= 0;
const cbl::Char * const		Win32PlatformWindow::sClassNameA	= "DelectableWindowClass";
const cbl::Wchar * const	Win32PlatformWindow::sClassNameW	= L"DelectableWindowClass";

Win32PlatformWindow::Win32PlatformWindow( GameWindow * const host, const GameWindowSettings & s )
: IPlatformWindow( host, s )
, mHwnd( NULL )
, mCursor( NULL )
, mIcon( NULL )
, mIsCursorIn( false )
{
	if( sWindowCount == 0 )
		RegisterWindowClass();

	int left   = ( GetDeviceCaps( GetDC( NULL ), HORZRES ) - s.Dimensions.Width )  / 2;
	int top    = ( GetDeviceCaps( GetDC( NULL ), VERTRES ) - s.Dimensions.Height ) / 2;

	DWORD w32style = WS_VISIBLE;

	if( s.Style & GameWindowStyle::None )
		w32style |= WS_POPUP;
	else
	{
		w32style |= WS_CAPTION | WS_MINIMIZEBOX;
		if( s.Style & GameWindowStyle::Resizable )	w32style |= WS_THICKFRAME | WS_MAXIMIZEBOX;
		if( s.Style & GameWindowStyle::Close )		w32style |= WS_SYSMENU;
	}
	
	RECT rc = { left, top, left + s.Dimensions.Width, top + s.Dimensions.Height };
	AdjustWindowRect( &rc, w32style, FALSE );

#if CBL_TEXT == CBL_TEXT_UTF16
    mHwnd = CreateWindowW( sClassNameW, L"", w32style, rc.left, rc.top, 
		rc.right - rc.left, rc.bottom - rc.top,
		NULL, NULL, GetModuleHandle(NULL), this );
#else
	mHwnd = CreateWindowA( sClassNameA, s.Title.c_str(), w32style, rc.left, rc.top, 
		rc.right - rc.left, rc.bottom - rc.top,
		NULL, NULL, GetModuleHandle(NULL), this );
#endif
	ShowWindow( mHwnd, SW_SHOW );
	ShowCursor( true );

	++sWindowCount;
}


GameWindowHandle Win32PlatformWindow::GetHandle( void )
{
	return reinterpret_cast< GameWindowHandle >( mHwnd );
}

Win32PlatformWindow::~Win32PlatformWindow()
{
	if( mHwnd )
		DestroyWindow( mHwnd );

	--sWindowCount;

	if( sWindowCount == 0 )
	{
#if CBL_TEXT == CBL_TEXT_UTF16
		UnregisterClassW( sClassNameW, GetModuleHandle( NULL ) );
#else
		UnregisterClassA( sClassNameA, GetModuleHandle( NULL ) );
#endif
	}
}

void Win32PlatformWindow::SetPosition( cbl::Int32 x, cbl::Int32 y )
{
	SetWindowPos( mHwnd, NULL, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER );
}

void Win32PlatformWindow::ResetSize( void )
{
	RECT rect = { 0, 0, mSettings.Dimensions.Width, mSettings.Dimensions.Height };
	AdjustWindowRect( &rect, GetWindowLong( mHwnd, GWL_STYLE ), false );
	SetWindowPos( mHwnd, NULL, 0, 0,
		mSettings.Dimensions.Width,
		mSettings.Dimensions.Height,
		SWP_NOMOVE | SWP_NOZORDER );
}

void Win32PlatformWindow::ResetTitle( void )
{
	SetWindowText( mHwnd, mSettings.Title.c_str() );
}

void Win32PlatformWindow::ResetStyle( void )
{
	DWORD w32style = WS_VISIBLE;

	if( mSettings.Style & GameWindowStyle::None )
		w32style |= WS_POPUP;
	else
	{
		w32style |= WS_CAPTION | WS_MINIMIZEBOX;
		if( mSettings.Style & GameWindowStyle::Resizable )	w32style |= WS_THICKFRAME | WS_MAXIMIZEBOX;
		if( mSettings.Style & GameWindowStyle::Close )		w32style |= WS_SYSMENU;
	}

	SetWindowLongPtr( mHwnd, GWL_STYLE, w32style );
}

void Win32PlatformWindow::ResetFullscreen( void )
{
	if( mSettings.Style & GameWindowStyle::Fullscreen )
	{
		DEVMODE dm;
		dm.dmSize       = sizeof(DEVMODE);
		dm.dmPelsWidth  = DWORD( mSettings.Dimensions.Width );
		dm.dmPelsHeight = DWORD( mSettings.Dimensions.Height );
		dm.dmBitsPerPel = DWORD( mSettings.Dimensions.BitsPerPixel );
		dm.dmFields     = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL;

		// Apply fullscreen mode
		if( ChangeDisplaySettings( &dm, CDS_FULLSCREEN ) != DISP_CHANGE_SUCCESSFUL )
		{
			mHost->OnWindowFullscreen( E::OnWindowFullscreen::ArgType( true, true ) );
			GLOG( cbl::LogLevel::Error << "Failed to change display mode for fullscreen." );

			return;
		}

		// Change window style (no border, no titlebar, ...)
		SetWindowLong( mHwnd, GWL_STYLE,   WS_POPUP );
		SetWindowLong( mHwnd, GWL_EXSTYLE, WS_EX_APPWINDOW );

		// And resize it so that it fits the entire screen
		SetWindowPos( mHwnd, HWND_TOP, 0, 0,
			mSettings.Dimensions.Width, mSettings.Dimensions.Height, SWP_FRAMECHANGED );
		ShowWindow( mHwnd, SW_SHOW );

		// SetPixelFormat can fail (really ?) if window style doesn't contain these flags
		long Style = GetWindowLong( mHwnd, GWL_STYLE );
		SetWindowLong( mHwnd, GWL_STYLE, Style | WS_CLIPCHILDREN | WS_CLIPSIBLINGS );
	}
	else
	{
		ChangeDisplaySettings( NULL, 0 );

		int left   = ( GetDeviceCaps( GetDC( NULL ), HORZRES ) - mSettings.Dimensions.Width )  / 2;
		int top    = ( GetDeviceCaps( GetDC( NULL ), VERTRES ) - mSettings.Dimensions.Height ) / 2;

		ResetStyle();
		SetPosition( left, top );
		ResetSize();
		ShowWindow( mHwnd, SW_SHOW );
	}
}

void Win32PlatformWindow::Show( bool state )
{
	ShowWindow( mHwnd, state ? SW_SHOW : SW_HIDE );
}

void Win32PlatformWindow::ShowCursor( bool state )
{
	if( state )
		mCursor = LoadCursor(NULL, IDC_ARROW);
	else
		mCursor = NULL;
}


void Win32PlatformWindow::ProcessWindowEvents( void )
{
	// We update the window only if it has been created.
	if( !mHwnd )
		return;

	MSG msg;
	while( PeekMessage( &msg, mHwnd, 0, 0, PM_REMOVE ) )
	{
		TranslateMessage( &msg );
		DispatchMessage( &msg );
	}
}

void Win32PlatformWindow::ProcessEvent( UINT msg, WPARAM wParam, LPARAM lParam )
{
	if( !mHwnd )
		return;

	switch( msg )
	{
		// Window close event.
		case WM_CLOSE:
		{
			mHost->OnWindowClosed();
			break;
		}

		// Set cursor event
		case WM_SETCURSOR:
		{
			// The mouse has moved, if the cursor is in our window we must refresh the cursor
			if (LOWORD(lParam) == HTCLIENT)
				SetCursor(mCursor);
			break;
		}

		// Window gain focus event.
		case WM_SETFOCUS:
		{
			mHost->OnWindowGainedFocus();
			break;
		}
		
		// Window lost focus event.
		case WM_KILLFOCUS:
		{
			mHost->OnWindowLostFocus();
			break;
		}

		// Window resize event.
		case WM_SIZE:
		{
			RECT dims;
			GetClientRect( mHwnd, &dims );
			mHost->OnWindowResized( E::OnWindowResized::ArgType(
				dims.right - dims.left,
				dims.bottom - dims.top
				) );
			break;
		}
		
		// Text event.
		case WM_CHAR:
		{
			mHost->OnWindowKeyChar( E::OnWindowKeyChar::ArgType(
				cbl::Uint32( wParam )
				) );
			break;
		}
		
		// Key down event.
		case WM_KEYDOWN:
		case WM_SYSKEYDOWN:
		{
			mHost->OnWindowKeyDown( E::OnWindowKeyDown::ArgType(
				ConvertVirtualCode( wParam, lParam )
				) );
			break;
		}
		
		// Key up event.
		case WM_KEYUP:
		case WM_SYSKEYUP:
		{
			mHost->OnWindowKeyUp( E::OnWindowKeyUp::ArgType(
				ConvertVirtualCode( wParam, lParam )
				) );
			break;
		}
	
		// Mouse wheel event.
		case WM_MOUSEWHEEL :
		{
			mHost->OnWindowMouseWheel( E::OnWindowMouseWheel::ArgType(
				static_cast< cbl::Int16 >( HIWORD( wParam ) ) / 120
				) );
			break;
		}

		// Mouse move event.
		case WM_MOUSEMOVE :
		{
			if( !mIsCursorIn )
			{
				// TODO: Finish tracking.
				TRACKMOUSEEVENT tme;
				tme.cbSize    = sizeof(TRACKMOUSEEVENT);
				tme.hwndTrack = mHwnd;
				tme.dwFlags   = TME_LEAVE;
				TrackMouseEvent(&tme);

				mIsCursorIn = true;
				mHost->OnWindowMouseEnter();
			}

			mHost->OnWindowMouseMove( E::OnWindowMouseMove::ArgType(
				LOWORD(lParam), HIWORD(lParam)
				) );
			break;
		}
	
		// Left mouse button down event.
		case WM_LBUTTONDOWN :
		{
			mHost->OnWindowMouseDown( E::OnWindowMouseDown::ArgType(
				Mouse::Left, LOWORD(lParam), HIWORD(lParam)
				) );
			break;
		}

		// Left mouse button up event.
		case WM_LBUTTONUP :
		{
			mHost->OnWindowMouseUp( E::OnWindowMouseUp::ArgType(
				Mouse::Left, LOWORD(lParam), HIWORD(lParam)
				) );
			break;
		}
	
		// Left mouse button down event.
		case WM_RBUTTONDOWN :
		{
			mHost->OnWindowMouseDown( E::OnWindowMouseDown::ArgType(
				Mouse::Right, LOWORD(lParam), HIWORD(lParam)
				) );
			break;
		}

		// Right mouse button up event.
		case WM_RBUTTONUP :
		{
			mHost->OnWindowMouseUp( E::OnWindowMouseUp::ArgType(
				Mouse::Right, LOWORD(lParam), HIWORD(lParam)
				) );
			break;
		}
	
		// Middle mouse button down event.
		case WM_MBUTTONDOWN :
		{
			mHost->OnWindowMouseDown( E::OnWindowMouseDown::ArgType(
				Mouse::Middle, LOWORD(lParam), HIWORD(lParam)
				) );
			break;
		}

		// Middle mouse button up event.
		case WM_MBUTTONUP :
		{
			mHost->OnWindowMouseUp( E::OnWindowMouseUp::ArgType(
				Mouse::Middle, LOWORD(lParam), HIWORD(lParam)
				) );
			break;
		}
	
		// X1 mouse button down event.
		case WM_XBUTTONDOWN :
		{
			mHost->OnWindowMouseDown( E::OnWindowMouseDown::ArgType(
				HIWORD(wParam) == XBUTTON1 ? Mouse::XButton1 : Mouse::XButton2,
				LOWORD(lParam), HIWORD(lParam)
				) );
			break;
		}

		// X2 mouse button up event.
		case WM_XBUTTONUP :
		{
			mHost->OnWindowMouseUp( E::OnWindowMouseUp::ArgType(
				HIWORD(wParam) == XBUTTON1 ? Mouse::XButton1 : Mouse::XButton2,
				LOWORD(lParam), HIWORD(lParam)
				) );
			break;
		}

		case WM_MOUSELEAVE:
		{
			mIsCursorIn = false;
			mHost->OnWindowMouseLeave();
			break;
		}
	}
}

void Win32PlatformWindow::RegisterWindowClass( void )
{
#if CBL_TEXT == CBL_TEXT_UTF16
	WNDCLASSW wc;
	wc.style         = 0;
	wc.lpfnWndProc   = &Win32PlatformWindow::WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = GetModuleHandle(NULL);
	wc.hIcon         = NULL;
	wc.hCursor       = 0;
	wc.hbrBackground = 0;
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = sClassNameW;
	RegisterClassW( &wc );
#else
	WNDCLASSA wc;
	wc.style         = 0;
	wc.lpfnWndProc   = &Win32PlatformWindow::WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = GetModuleHandle(NULL);
	wc.hIcon         = NULL;
	wc.hCursor       = 0;
	wc.hbrBackground = 0;
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = sClassNameA;
	RegisterClassA( &wc );
#endif
	++sWindowCount;
}

dbl::Key::Code ConvertVirtualCode( WPARAM virtualKey, LPARAM flags )
{
	switch( virtualKey )
	{
		// VK_SHIFT is handled by the GetShiftState function
		case VK_MENU :       return (flags & (1 << 24)) ? Key::RAlt		: Key::LAlt;
		case VK_CONTROL :    return (flags & (1 << 24)) ? Key::RCtrl	: Key::LCtrl;
		case VK_LWIN :       return Key::LSystem;
		case VK_RWIN :       return Key::RSystem;
		case VK_APPS :       return Key::Menu;
		case VK_OEM_1 :      return Key::SemiColon;
		case VK_OEM_2 :      return Key::Slash;
		case VK_OEM_PLUS :   return Key::Equal;
		case VK_OEM_MINUS :  return Key::Dash;
		case VK_OEM_4 :      return Key::LBracket;
		case VK_OEM_6 :      return Key::RBracket;
		case VK_OEM_COMMA :  return Key::Comma;
		case VK_OEM_PERIOD : return Key::Period;
		case VK_OEM_7 :      return Key::Quote;
		case VK_OEM_5 :      return Key::BackSlash;
		case VK_OEM_3 :      return Key::Tilde;
		case VK_ESCAPE :     return Key::Escape;
		case VK_SPACE :      return Key::Space;
		case VK_RETURN :     return Key::Return;
		case VK_BACK :       return Key::Back;
		case VK_TAB :        return Key::Tab;
		case VK_PRIOR :      return Key::PageUp;
		case VK_NEXT :       return Key::PageDown;
		case VK_END :        return Key::End;
		case VK_HOME :       return Key::Home;
		case VK_INSERT :     return Key::Insert;
		case VK_DELETE :     return Key::Delete;
		case VK_ADD :        return Key::Add;
		case VK_SUBTRACT :   return Key::Subtract;
		case VK_MULTIPLY :   return Key::Multiply;
		case VK_DIVIDE :     return Key::Divide;
		case VK_PAUSE :      return Key::Pause;
		case VK_F1 :         return Key::F1;
		case VK_F2 :         return Key::F2;
		case VK_F3 :         return Key::F3;
		case VK_F4 :         return Key::F4;
		case VK_F5 :         return Key::F5;
		case VK_F6 :         return Key::F6;
		case VK_F7 :         return Key::F7;
		case VK_F8 :         return Key::F8;
		case VK_F9 :         return Key::F9;
		case VK_F10 :        return Key::F10;
		case VK_F11 :        return Key::F11;
		case VK_F12 :        return Key::F12;
		case VK_F13 :        return Key::F13;
		case VK_F14 :        return Key::F14;
		case VK_F15 :        return Key::F15;
		case VK_LEFT :       return Key::Left;
		case VK_RIGHT :      return Key::Right;
		case VK_UP :         return Key::Up;
		case VK_DOWN :       return Key::Down;
		case VK_NUMPAD0 :    return Key::Numpad0;
		case VK_NUMPAD1 :    return Key::Numpad1;
		case VK_NUMPAD2 :    return Key::Numpad2;
		case VK_NUMPAD3 :    return Key::Numpad3;
		case VK_NUMPAD4 :    return Key::Numpad4;
		case VK_NUMPAD5 :    return Key::Numpad5;
		case VK_NUMPAD6 :    return Key::Numpad6;
		case VK_NUMPAD7 :    return Key::Numpad7;
		case VK_NUMPAD8 :    return Key::Numpad8;
		case VK_NUMPAD9 :    return Key::Numpad9;
		case 'A' :           return Key::A;
		case 'Z' :           return Key::Z;
		case 'E' :           return Key::E;
		case 'R' :           return Key::R;
		case 'T' :           return Key::T;
		case 'Y' :           return Key::Y;
		case 'U' :           return Key::U;
		case 'I' :           return Key::I;
		case 'O' :           return Key::O;
		case 'P' :           return Key::P;
		case 'Q' :           return Key::Q;
		case 'S' :           return Key::S;
		case 'D' :           return Key::D;
		case 'F' :           return Key::F;
		case 'G' :           return Key::G;
		case 'H' :           return Key::H;
		case 'J' :           return Key::J;
		case 'K' :           return Key::K;
		case 'L' :           return Key::L;
		case 'M' :           return Key::M;
		case 'W' :           return Key::W;
		case 'X' :           return Key::X;
		case 'C' :           return Key::C;
		case 'V' :           return Key::V;
		case 'B' :           return Key::B;
		case 'N' :           return Key::N;
		case '0' :           return Key::Num0;
		case '1' :           return Key::Num1;
		case '2' :           return Key::Num2;
		case '3' :           return Key::Num3;
		case '4' :           return Key::Num4;
		case '5' :           return Key::Num5;
		case '6' :           return Key::Num6;
		case '7' :           return Key::Num7;
		case '8' :           return Key::Num8;
		case '9' :           return Key::Num9;
	}

	return Key::Code( 0 );
}

LRESULT CALLBACK Win32PlatformWindow::WindowProc( HWND handle, UINT msg, WPARAM wParam, LPARAM lParam )
{
	// Associate handle and Window instance when the creation message is received
	if( msg == WM_CREATE )
	{
		// Get WindowImplWin32 instance (it was passed as the last argument of CreateWindow)
		long This = reinterpret_cast<long>(reinterpret_cast<CREATESTRUCT*>(lParam)->lpCreateParams);

		// Set as the "user data" parameter of the window
		SetWindowLongPtr( handle, GWLP_USERDATA, This );
	}

	// Get the WindowImpl instance corresponding to the window handle
	Win32PlatformWindow * window = reinterpret_cast< Win32PlatformWindow *>( GetWindowLongPtr( handle, GWLP_USERDATA ) );

	// Forward the event to the appropriate function
	if( window )
		window->ProcessEvent( msg, wParam, lParam);

	// We don't forward the WM_CLOSE message to prevent the OS from automatically destroying the window
	if( msg == WM_CLOSE )
		return 0;

#if CBL_TEXT == CBL_TEXT_UTF16
	return DefWindowProcW( handle, msg, wParam, lParam );
#else
	return DefWindowProcA( handle, msg, wParam, lParam );
#endif
}
